/*
 * Copyright 2006 Kevin Wetzels 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0 
 *       
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package be.roam.drest.xml.rss;

import java.text.ParseException;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import be.roam.util.DateUtil;
import be.roam.util.StringUtil;

/**
 * SAX handler for RSS 2.0 feeds as specified at 
 * <a href="http://blogs.law.harvard.edu/tech/rss" title="">http://blogs.law.harvard.edu/tech/rss</a>. 
 * <p>
 * The handler takes care of retrieving the common values from an RSS 2.0 feed. Values that
 * are not taken into consideration include <code>rating</code> and <code>textInput</code>. 
 * </p>
 * <p>
 * To parse a RSS 2.0 feed have look at the {@link Rss20HandlerImpl} class.
 * </p>
 * 
 * @see Rss20HandlerImpl
 * 
 * @author Kevin Wetzels
 * @version 1.0
 */
public abstract class Rss20Handler<FeedType extends Rss20Feed<? extends Rss20Channel<? extends Rss20Item>>, ChannelType extends Rss20Channel<? extends Rss20Item>, ItemType extends Rss20Item> extends DefaultHandler { 
    
    /**
     * The feed object.
     */
    protected FeedType feed;
    
    /**
     * The current channel being parsed.
     */
    protected ChannelType currentChannel;
    
    /**
     * The current item being parsed.
     */
    protected ItemType currentItem;
    
    /**
     * Buffer for character data.
     */
    protected StringBuilder characterBuffer;
    
    /**
     * Flag to signal whether we're inside a <code>image</code> element.
     */
    protected boolean inImage;
    
    /**
     * Constructor.
     * 
     * @param feed the feed object to fill with data.
     */
    public Rss20Handler(FeedType feed) {
        this.feed = feed;
    }

    /**
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if ("channel".equals(localName)) {
            currentChannel = createChannelObject();
            addChannelToFeed(currentChannel);
        } else if ("item".equals(localName)) {
            currentItem = createItemObject();
            addItemToChannel(currentItem);
        } else if ("title".equals(localName) || "link".equals(localName) || "description".equals(localName) 
                || "language".equals(localName) || "url".equals(localName) || "pubDate".equals(localName)
                || "lastBuildDate".equals(localName) || "ttl".equals(localName) || "hour".equals(localName)
                || "day".equals(localName) || "author".equals(localName) || "comments".equals(localName)) {
            characterBuffer = new StringBuilder();
        } else if ("category".equals(localName)) {
            characterBuffer = new StringBuilder();
            if (currentItem == null) {
                currentChannel.setCategoryDomain(attributes.getValue("domain"));
            } else {
                currentItem.setCategoryDomain(attributes.getValue("domain"));
            }
        } else if ("image".equals(localName)) {
            inImage = true;
        } else if ("guid".equals(localName)) {
            characterBuffer = new StringBuilder();
            currentItem.setGuidPermaLink(StringUtil.parseBoolean(attributes.getValue("isPermaLink")));
        } else if ("source".equals(localName)) {
            characterBuffer = new StringBuilder();
            currentItem.setSourceUrl(attributes.getValue("url"));
        } else if ("enclosure".equals(localName)) {
            currentItem.setEnclosureUrl(attributes.getValue("url"));
            currentItem.setEnclosureSizeInBytes(attributes.getValue("length"));
            currentItem.setEnclosureType(attributes.getValue("type"));
        } else if ("cloud".equals(localName)) {
            currentChannel.setCloudDomain(attributes.getValue("domain"));
            currentChannel.setCloudPath(attributes.getValue("path"));
            currentChannel.setCloudPort(attributes.getValue("port"));
            currentChannel.setCloudRegisterProcedure(attributes.getValue("registerProcedure"));
            currentChannel.setCloudProtocol(attributes.getValue("protocol"));
        }
    }    

    /**
     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("channel".equals(localName)) {
            currentChannel = null;
        } else if ("item".equals(localName)) {
            currentItem = null;
        } else if ("title".equals(localName)) {
            if (currentItem == null && !inImage) {
                currentChannel.setTitle(getCharacters(true));
            } else if (inImage) {
                currentChannel.setImageTitle(getCharacters(true));
            } else {
                currentItem.setTitle(getCharacters(true));
            }
        } else if ("link".equals(localName)) {
            if (currentItem == null && !inImage) {
                currentChannel.setLink(getCharacters(true));
            } else if (inImage) {
                currentChannel.setImageLink(getCharacters(true));
            } else {
                currentItem.setLink(getCharacters(true));
            }
        } else if ("description".equals(localName)) {
            if (currentItem == null && !inImage) {
                currentChannel.setDescription(getCharacters(true));
            } else if (inImage) {
                currentChannel.setImageDescription(getCharacters(true));
            } else {
                currentItem.setDescription(getCharacters(true));
            }
        } else if ("pubDate".equals(localName)) {
            String dateString = null;
            try {
                dateString = getCharacters(true);
                Date date = DateUtil.parseDateRfc822(dateString);
                if (currentItem == null) {                    
                    currentChannel.setDatePublication(date);
                } else {
                    currentItem.setDatePublication(date);
                }
            } catch (ParseException e) {
                // Do nothing
            }
        } else if ("category".equals(localName)) {
            if (currentItem == null) {                    
                currentChannel.setCategory(getCharacters(true));
            } else {
                currentItem.setCategory(getCharacters(true));
            }
        } else if ("image".equals(localName)) {
            inImage = false;
        } else if ("language".equals(localName)) {
            currentChannel.setLanguage(getCharacters(true));
        } else if ("url".equals(localName)) {
            currentChannel.setImageUrl(getCharacters(true));
        } else if ("width".equals(localName)) {
            currentChannel.setImageWidth(Integer.parseInt(getCharacters(true)));
        } else if ("height".equals(localName)) {
            currentChannel.setImageWidth(Integer.parseInt(getCharacters(true)));
        } else if ("copyright".equals(localName)) {
            currentChannel.setCopyright(getCharacters(true));
        } else if ("managingEditor".equals(localName)) {
            currentChannel.setManagementEditor(getCharacters(true));
        } else if ("webMaster".equals(localName)) {
            currentChannel.setWebMaster(getCharacters(true));
        } else if ("lastBuildDate".equals(localName)) {
            String dateString = null;
            try {
                dateString = getCharacters(true);
                currentChannel.setDateLastBuild(DateUtil.parseDateRfc822(dateString));
            } catch (ParseException e) {
                // Do nothing
            }
        } else if ("ttl".equals(localName)) {
            currentChannel.setTimeToLive(Integer.parseInt(getCharacters(true)));
        } else if ("hour".equals(localName)) {
            if (currentChannel.getSkipHours() == null) {
                Set<Integer> skipHours = new HashSet<Integer>();
                currentChannel.setSkipHours(skipHours);
            }
            currentChannel.getSkipHours().add(new Integer(getCharacters(true)));
        } else if ("day".equals(localName)) {
            if (currentChannel.getSkipDays() == null) {
                Set<Integer> skipDays = new HashSet<Integer>();
                currentChannel.setSkipDays(skipDays);
            }
            currentChannel.getSkipDays().add(new Integer(getCharacters(true)));
        } else if ("author".equals(localName)) {
            currentItem.setAuthor(getCharacters(true));
        } else if ("comments".equals(localName)) {
            currentItem.setCommentsUrl(getCharacters(true));
        } else if ("guid".equals(localName)) {
            currentItem.setGuid(getCharacters(true));
        } else if ("source".equals(localName)) {
            currentItem.setSource(getCharacters(true));
        }
    }
    
    /**
     * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (characterBuffer != null) {
            characterBuffer.append(ch, start, length);
        }
    }
    
    /**
     * Retrieves the collected character data and optionally clear
     * the character buffer.
     * 
     * @param clearCharacters should the buffer be cleared or not?
     * 
     * @return the collected character data
     */
    protected String getCharacters(boolean clearCharacters) {
        String result = characterBuffer.toString();
        if (clearCharacters) {
            characterBuffer = null;
        }
        return result;
    }
    
    /**
     * Creates the object associated with a channel.
     * <p>
     * Override this method to create a specific channel class when 
     * you're subclassing this class and need to retrieve some data 
     * from the channel that's not associated with the RSS 2.0 spec.
     * </p>
     * 
     * @return a new {@link Rss20Channel} object.
     */
    protected abstract ChannelType createChannelObject();
    
    /**
     * Creates the object associated with an item.
     * <p>
     * Override this method to create a specific item class when 
     * you're subclassing this class and need to retrieve some data 
     * from the item that's not associated with the RSS 2.0 spec.
     * </p>
     * 
     * @return a new {@link Rss20Item} object.
     */
    protected abstract ItemType createItemObject();
    
    /**
     * Adds an item to the current channel.
     * 
     * @param item item to add to the current channel
     */
    protected abstract void addItemToChannel(ItemType item);
    
    /**
     * Adds a channel to the feed.
     * 
     * @param channel channel to add to the feed
     */
    protected abstract void addChannelToFeed(ChannelType channel);
    
}
